#!/bin/bash
#
# Copyright (c) Microsoft. All rights reserved.
#
# Licensed under the MIT license. See LICENSE.md file in the project root
# for full license information.
# ==============================================================================
#
# TODO --math-library support on Windows

# Setting some default values
BUILD=1
RUN=1
CLEAN_AFTER=0
CLEAN_BEFORE=0
RANDOM_OUTPUT=0
BUILD_VERSION=
CODE_COVERAGE=no
FLAVORS="debug:release"
TARGETS="cpu:gpu"
MATH_LIBRARY="mkl"
TESTTARGETS="cpu:gpu"
EXTRA_CONFIGURE_OPTIONS=

# parsing command line arguments:
while [[ $# > 0 ]]
do
key="$1"

case $key in
    -h|--help)
    echo "Usage: build-and-test [options]"
    echo "Options:"
    echo "  -q|--quiet-build - redirect build output to file"
    echo "  -r|--run-only - elides build step, runs the binaries that have already been built"
    echo "  -b|--build-only - just build, do not run"
    echo "  -f|--flavors <flavor1:flavor2...> - which flavor to build (by default $FLAVORS)"
    echo "  -t|--targets <target1:target2...> - which target to build (by default $TARGETS)"
    echo "  -m|--math-library <mathlibrary> - which math library to build with (by default $MATH_LIBRARY)"
    echo "  -tt|--test-targets <testtarget1:testtarget2...> - which target to test (by default $TESTTARGETS)"
    echo "  -cc|--code-coverage - build with support for code coverage (gcov)"
    echo "  -cb|--clean-build - clean up the enlistment binaries before build"
    echo "  -cba|--clean-build-after - clean up the enlistment binaries after build"
    echo "  -bv|--build-version <build_version> - build using given version assuming public availablity (default hard-coded in configure file)"
    echo "  -rnd|--random-output-suffix - add random suffix to output directory"
    echo "  -o|--output-directory <output_dir> - specify output directory to use (by default those will be in <cntk_root>.run-<operating_system>)"
    echo "  -x|--extra-configure-options <options> - extra options to pass to configure"
    echo "The root directory used to build and run CNTK is hosts the Scripts directory that contains this script"
    exit 1
    ;;
    -q|--quiet)
    QUIET_BUILD=1
    ;;
    -r|--run-only)
    BUILD=0
    RUN=1
    ;;
    -rnd|--random-output-suffix)
    RANDOM_OUTPUT=1
    ;;
    -b|--build-only)
    BUILD=1
    RUN=0
    ;;
    -cb|--clean-build)
    CLEAN_BEFORE=1
    BUILD=1
    ;;
    -cba|--clean-build-after)
    CLEAN_AFTER=1
    BUILD=1
    ;;
    -bv|--build-version)
    BUILD_VERSION="${2,,}"
    shift # past argument
    ;;
    -f|--flavors)
    FLAVORS="${2,,}"
    shift # past argument
    ;;
    -t|--targets)
    TARGETS="${2,,}"
    shift # past argument
    ;;
    -m|--math-library)
    case ${2,,} in
        mkl)
        MATH_LIBRARY_OPTION="--with-mkl=$MKL_PATH"
        ;;
        mkl-sequential)
        MATH_LIBRARY_OPTION="--with-mkl-sequential=$MKL_PATH"
        ;;
        *)
        echo Unknown math library $MATH_LIBRARY
        exit 1
        ;;
    esac
    shift # past argument
    ;;
    -tt|--test-targets)
    TESTTARGETS="${2,,}"
    shift # past argument
    ;;
    -cc|--code-coverage)
    CODE_COVERAGE=yes
    ;;
    -x|--extra-configure-options)
    EXTRA_CONFIGURE_OPTIONS="$2"
    shift # past argument
    ;;
    -o|--output-directory)
    OUTPUT_DIR="$2"
    shift # past argument
    ;;
    *)
    echo Unknown option $key
    exit 1
    ;;
esac
shift # past argument or value
done

# Step 0 -- Validate all necessary prerequisites and check for incompatible options
# It is possible to use this script on Windows to build CNTK
# from Cygwin window with Visual C++ environment loaded.
# In that case OS environment variable will be set and we
# can use it to differentiate from Linux.
if [[ $CLEAN_BEFORE == 1 && $RUN == 1 && $BUILD == 0 ]]; then
    echo "============ ERROR: Incompatible options RUN and CLEAN_BEFORE set without BUILD ============"
    exit 1
fi

if [[ $OS == "Windows_NT" && $OSTYPE == "cygwin" ]]; then
    DEBUG_DIR=Debug
    RELEASE_DIR=Release
    PREFIX_DIR=x64
    BIN_NAME=CNTK.exe
    BUILD_OS="windows"

    if [[ $VS140COMNTOOLS == "" ]]; then
        echo "============ Visual Studio 14.0 environment not properly setup or VS not installed ============"
        echo "============ Please find and run the appropriate vcvarsall.bat script ============"
        exit 1
    fi

elif [[ $OSTYPE == "linux-gnu" ]]; then
    DEBUG_DIR=build/$TARGET/debug
    RELEASE_DIR=build/$TARGET/release
    PREFIX_DIR=
    # Make sure no dependencies on current directory
    BIN_NAME=bin/cntk
    MAKEFILE=Makefile
    BUILD_OS="linux"
else
    echo "============ ERROR: Unsupported OS ============"
    echo "============ Script supports only building from Linux and Windows through Cygwin ============"
    exit 1
fi

# Step 1 -- Prepare temporary folders and files, tweak settings if necessary

# Get to the root path from which we know how to build and run
SCRIPT=`readlink -f $0`
SCRIPT_DIR=`dirname $SCRIPT`
CNTK_ROOT=`dirname $SCRIPT_DIR`

# Setup the output directory
if [[ $OUTPUT_DIR == "" ]]; then
    OUTPUT_DIR="$CNTK_ROOT/.run-$BUILD_OS"
fi

# Add random number at the end of the output directory to prevent overwriting previous results
if [[ $RANDOM_OUTPUT == 1 ]]; then
    OUTPUT_DIR="$OUTPUT_DIR-$RANDOM"
fi

echo "============ Creating CNTK temp directory in $OUTPUT_DIR ============"
mkdir -p $OUTPUT_DIR || exit $?

CONF_FILE="$OUTPUT_DIR/OneHidden.cntk"
BUILD_FILE="$OUTPUT_DIR/Build"
RUN_FILE="$OUTPUT_DIR/Result"

if ! [[ -d "$CNTK_ROOT/Source" ]]; then
    echo "============ ERROR: Build script located in the wrong directory ($SCRIPT_DIR) ============"
    exit 1
fi

cd $CNTK_ROOT

if ! [[ -f $CONF_FILE ]]; then
    cp Tests/EndToEndTests/Simple2d/OneHidden.cntk $CONF_FILE || exit $?

    # This chmod is necessary due to restrictive Cygwin interpretation of Windows permissions.
    # Cygwin interprets Windows permissions as ----rwx---, which lacks read permissions for user.
    chmod a+r $CONF_FILE || exit $?
fi

if [[ $QUIET_BUILD == 1 ]]; then
    echo "============ WARNING: You have selected quiet build. All build output will be placed in ($OUTPUT_DIR) ============"
fi

# Initialize lists of flavors and targets
flavorArray=(${FLAVORS//:/ })
targetArray=(${TARGETS//:/ })
testTargetArray=(${TESTTARGETS//:/ })

# Step 2 -- Build the project for all requested flavors and targets
if [[ $BUILD == 1 ]]; then
    for FLAVOR in "${flavorArray[@]}"
    do
        for TARGET in "${targetArray[@]}"
        do
            echo "============ Building CNTK $TARGET/$FLAVOR (clean=$CLEAN_BEFORE)  ============"

            # Our make is too noisy right now and it is difficult to spot
            # issues from stdout and stderr. In the quiet mode these are
            # redirected to a file where they could be examined after the fact
            if [[ $QUIET_BUILD == 1 ]]; then
                exec 6>$BUILD_FILE.$FLAVOR.out || exit $?
                exec 7>$BUILD_FILE.$FLAVOR.err || exit $?
            else
                exec 6>&1 || exit $?
                exec 7>&2 || exit $?
            fi

            if [[ $OS == "Windows_NT" ]]; then
                if [[ $TARGET == "cpu" ]]; then
                    CONFIGURATION=${FLAVOR}_CpuOnly
                else
                    CONFIGURATION=${FLAVOR}
                fi

                if [[ $CLEAN_BEFORE == 1 ]]; then
                    msbuild.exe /nologo /verbosity:m /property:Configuration=$CONFIGURATION /t:Clean 1>&6 2>&7 || exit $?
                fi
                msbuild.exe /nologo /verbosity:m /m /property:Configuration=$CONFIGURATION 1>&6 2>&7 || exit $?
            else
                BUILD_DIR=$CNTK_ROOT/build/$TARGET/$FLAVOR
                if [[ $TARGET == "cpu" ]]; then
                    CUDAOPT=no
                else
                    CUDAOPT=yes
                fi
                ./configure --with-build-top=$BUILD_DIR ${MATH_LIBRARY_OPTION} --with-buildtype=$FLAVOR --cuda=$CUDAOPT --with-code-coverage=$CODE_COVERAGE --with-build-version=$BUILD_VERSION $EXTRA_CONFIGURE_OPTIONS
                if [[ $CLEAN_BEFORE == 1 ]]; then
                    make -C $BUILD_DIR -f $MAKEFILE clean 1>&6 2>&7 || exit $?
                fi
                make -C $BUILD_DIR -j `nproc` -f $MAKEFILE 1>&6 2>&7 || exit $?

            fi
            if [[ $QUIET_BUILD == 1 ]]; then
              chmod a+r $BUILD_FILE.*
            fi
        done
    done
fi

# Step 3 -- Run the project tests, both debug and release, if requested
if [[ $RUN == 1 ]]; then

    cd $PREFIX_DIR
    echo "============ cp Tests/EndToEndTests/Simple2d/OneHidden.cntk $CONF_FILE  ============"
    echo "============ cd $CNTK_ROOT/Tests/EndToEndTests/Simple2d/Data  ============"

    for FLAVOR in "${flavorArray[@]}"
    do
        for TARGET in "${targetArray[@]}"
        do
            for TESTTARGET in "${testTargetArray[@]}"
            do
                # Determine how to set the deviceId parameter in the configuration.
                # 0 will pick the first GPU (or fail. -1 will pick the CPU.
                if [[ $TESTTARGET == gpu ]]; then
                    DEVICE_ID=0
                else
                    DEVICE_ID=-1
                fi

                if [[ $TESTTARGET == gpu && $TARGET == cpu ]]; then
                    # CPU-only builds cannot run GPU test targets
                    continue;
                fi

                if [[ $OSTYPE == "linux-gnu" ]]; then
                    FLAVOR_DIR=build/$TARGET/$FLAVOR
                else
                    if [[ $FLAVOR == "debug" ]]; then
                        FLAVOR_DIR="$DEBUG_DIR"
                    else
                        FLAVOR_DIR="$RELEASE_DIR"
                    fi
                    if [[ $TARGET == cpu ]]; then
                        FLAVOR_DIR="${FLAVOR}_CpuOnly"
                    fi
                fi
                OUT_FILE="$RUN_FILE.$FLAVOR.$TARGET.$TESTTARGET.out"

                BIN_PATH=$CNTK_ROOT/$PREFIX_DIR/$FLAVOR_DIR/$BIN_NAME
                if ! [[ -f $BIN_PATH ]]; then
                    echo "============ ERROR: CNTK did not build properly for $TARGET/$FLAVOR ============"
                    echo "Missing file: $BIN_PATH"
                    exit 1
                fi

                if [[ $OS == "Windows_NT" ]]; then
                    echo "============ Running $BIN_PATH configFile=`cygpath -w $CONF_FILE` for $TARGET/$FLAVOR (on $TESTTARGET) ============"
                else
                    echo "============ Running $BIN_PATH configFile=$CONF_FILE for $TARGET/$FLAVOR (on $TESTTARGET) ============"
                fi
                echo "============ output in ($OUT_FILE) ============"
                cd $CNTK_ROOT/Tests/EndToEndTests/Simple2d/Data
                rm -rf "$OUTPUT_DIR/Models"
                if [[ $OS == "Windows_NT" ]]; then
                    # We have to use cygpath on Windows to modify the file paths into the format readable by cntk.
                    time $BIN_PATH configFile="`cygpath -w $CONF_FILE`" deviceId=$DEVICE_ID OutputDir="`cygpath -w $OUTPUT_DIR`" &>$OUT_FILE || exit $?
                else
                    time $BIN_PATH configFile=$CONF_FILE deviceId=$DEVICE_ID OutputDir="$OUTPUT_DIR" &>$OUT_FILE || exit $?
                fi
                chmod a+r $RUN_FILE.*

                # Check if execution was successful
                grep -q "Using ${TESTTARGET^^}" "$OUT_FILE" || {
                    echo "============ ERROR: Run output (in $OUT_FILE) did not contain information about target device ($TESTTARGET) ============"
                    exit 1
                }

                grep -q "EXCEPTION" "$OUT_FILE" && {
                    echo "============ ERROR: Run output in ($OUT_FILE) contains exceptions ============"
                    grep "EXCEPTION" "$OUT_FILE"
                    exit 1
                }
            done
        done
    done
fi

# Step 5 -- Optionally clean after builds and tests
if [[ $CLEAN_AFTER == 1 ]]; then
    cd $CNTK_ROOT
    for FLAVOR in "${flavorArray[@]}"
    do
        for TARGET in "${targetArray[@]}"
        do
            echo "============ Cleaning up CNTK $TARGET/$FLAVOR  ============"
            if [[ $OS == "Windows_NT" ]]; then
                if [[ $TARGET == "cpu" ]]; then
                    CONFIGURATION=${FLAVOR}_CpuOnly
                else
                    CONFIGURATION=${FLAVOR}
                fi
                msbuild.exe /nologo /verbosity:m /property:Configuration=$CONFIGURATION /t:clean 1>&6 2>&7 || exit $?
            else
                make -C build/$TARGET/$FLAVOR -f $MAKEFILE clean 1>&6 2>&7 || exit $?
            fi
        done
    done
    rm -rf "$OUTPUT_DIR"
fi

echo "============ Build and test of CNTK was successful!  ============"
